﻿using System;
using CSharpExt.Collections;

namespace CSharpExt.Time.Scheduler
{
    public abstract class Trigger
    {
        #region Private Data

        public static readonly DateTime NO_DATETIME = new DateTime(0);

        protected DateTime _startDate = NO_DATETIME;
        protected DateTime _endDate = NO_DATETIME;

        #endregion

        #region Properties

        public virtual DateTime When
        {
            set { _startDate = value; }
        }

        public DateTime StartDate
        {
            set { _startDate = value; }
        }

        public DateTime EndDate
        {
            set { _endDate = value; }
        }

        #endregion

        #region Interface

        public virtual bool IsValid(DateTime now)
        {
            if (_startDate != NO_DATETIME && _endDate != NO_DATETIME)
                return _startDate <= now && now <= _endDate;

            if (_startDate != NO_DATETIME)
                return _startDate <= now;

            if (_endDate != NO_DATETIME)
                return now <= _endDate;

            return true;
        }

        #endregion

        #region Implementation

        /// <summary>
        /// Ritorna un intero che rappresenta il numero di mesi che sono 
        /// passato dalla data 1/1/0000 alla data indicata da dt
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        protected int gregorianMonth(DateTime dt)
        {
            int year = dt.Year - 1;
            return year * 12 + dt.Month - 1;
        }

        /// <summary>
        /// Ritorna il numero di settimane che sono passati dalla data 1/1/0000
        /// alla data indicata da dt.
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        protected int gregorianWeek(DateTime dt)
        {
            return gregorianDay(dt) / 7;
        }

        /// <summary>
        /// Ritorna il numero di giorni che sono passati dalla data 1/1/0000
        /// alla data indicata da dt.
        /// 
        /// Nota: c'e' anche la correzione per gli anni bisestili.
        /// Un anno bistestile e' tale se l'anno e':
        /// - divisibile per 4
        /// - NON divisibile per 100 (cambio di secolo)
        /// - MA divisibile per 400
        /// </summary>
        /// <param name="dt"></param>
        /// <returns></returns>
        protected int gregorianDay(DateTime dt)
        {
            int year = dt.Year - 1;
            return year * 365 + year / 4 - year / 100 + year / 400 + dt.DayOfYear;
        }

        #endregion
    }

    public class OneTimeTrigger : Trigger
    {
        #region Properties

        public override DateTime When
        {
            set
            {
                _startDate = value;
                _endDate = _startDate;
            }
        }

        #endregion

        #region Interface

        public override bool IsValid(DateTime now)
        {
            return now == _startDate;
        }

        #endregion
    }

    public class DailyTrigger : Trigger
    {
        #region Private Data

        private int _second;
        private int _minute;
        private int _hour;
        private int _gregorianDay;
        private int _repeatDays = 1;

        #endregion

        #region Properties

        public override DateTime When
        {
            set
            {
                _startDate = value;
                _second = _startDate.Second;
                _minute = _startDate.Minute;
                _hour = _startDate.Hour;
                _gregorianDay = gregorianDay(_startDate);
            }
        }

        public int RepeatEach
        {
            set { _repeatDays = value; }
        }

        #endregion

        #region Interface

        public override bool IsValid(DateTime now)
        {
            if (!base.IsValid(now))
                return false;

            int second = now.Second;
            int minute = now.Minute;
            int hour = now.Hour;
            int gday = gregorianDay(now);

            if (_second != second)
                return false;

            if (_minute != minute)
                return false;

            if (_hour != hour)
                return false;

            if ((gday - _gregorianDay) % _repeatDays != 0)
                return false;

            return true;
        }

        #endregion
    }

    public class WeeklyTrigger : Trigger
    {
        #region Private Data

        private int _second;
        private int _minute;
        private int _hour;

        private int _gregorianWeek;
        private int _repeatWeeks = 1;
        private BitSet _weekDays = BitSet.All(7);

        #endregion

        #region Properties

        public override DateTime When
        {
            set
            {
                _startDate = value;
                _second = _startDate.Second;
                _minute = _startDate.Minute;
                _hour = _startDate.Hour;
                _gregorianWeek = gregorianWeek(_startDate);
            }
        }

        public int RepeatEach
        {
            set { _repeatWeeks = value; }
        }

        public BitSet WeekDays
        {
            set { _weekDays = value; }
        }

        #endregion

        #region Interface

        public override bool IsValid(DateTime now)
        {
            if (!base.IsValid(now))
                return false;

            int second = now.Second;
            int minute = now.Minute;
            int hour = now.Hour;
            int gweek = gregorianWeek(now);
            int wday = (int)now.DayOfWeek;

            if (_second != second)
                return false;

            if (_minute != minute)
                return false;

            if (_hour != hour)
                return false;

            if ((gweek - _gregorianWeek) % _repeatWeeks != 0)
                return false;

            if (!_weekDays.Test(wday))
                return false;

            return true;
        }

        #endregion
    }

    public class MonthlyTrigger : Trigger
    {
        #region Private Data

        private int _second;
        private int _minute;
        private int _hour;

        private int _reapeatMonths = 1;
        private BitSet _months = BitSet.All(12, 1);
        private BitSet _days = BitSet.All(31, 1);
        private BitSet _weekDays = BitSet.All(7);
        private int _gregorianMonth;

        #endregion

        #region Properties

        public override DateTime When
        {
            set
            {
                _startDate = value;
                _second = _startDate.Second;
                _minute = _startDate.Minute;
                _hour = _startDate.Hour;
                _gregorianMonth = gregorianMonth(_startDate);
            }
        }

        public int RepeatEach
        {
            set { _reapeatMonths = value; }
        }

        public BitSet Months
        {
            set { _months = value; }
        }

        public BitSet Days
        {
            set { _days = value; }
        }

        public BitSet WeekDays
        {
            set { _weekDays = value; }
        }

        #endregion

        #region Implementation

        public override bool IsValid(DateTime now)
        {
            if (!base.IsValid(now))
                return false;

            int second = now.Second;
            int minute = now.Minute;
            int hour = now.Hour;
            int day = now.Day;
            int month = now.Month;
            int wday = (int)now.DayOfWeek;
            int gmonth = gregorianMonth(now);

            if (_second != second)
                return false;

            if (_minute != minute)
                return false;

            if (_hour != hour)
                return false;

            if ((gmonth - _gregorianMonth) % _reapeatMonths != 0)
                return false;

            if (!_days.Test(day))
                return false;

            if (!_weekDays.Test(wday))
                return false;

            if (!_months.Test(month))
                return false;

            return true;
        }

        #endregion
    }

    public class CronTrigger : Trigger
    {
        #region Private Data

        private BitSet _seconds = BitSet.All(60);
        private BitSet _minutes = BitSet.All(60);
        private BitSet _hours = BitSet.All(24);
        private BitSet _days = BitSet.All(31, 1);
        private BitSet _months = BitSet.All(12, 1);
        private BitSet _weekDays = BitSet.All(7);

        #endregion

        #region Constructor

        public CronTrigger()
        {

        }

        #endregion

        #region Properties

        public BitSet Seconds
        {
            set { _seconds = value; }
        }

        public BitSet Minutes
        {
            set { _minutes = value; }
        }

        public BitSet Hours
        {
            set { _hours = value; }
        }

        public BitSet Days
        {
            set { _days = value; }
        }

        public BitSet Months
        {
            set { _months = value; }
        }

        public BitSet WeekDays
        {
            set { _weekDays = value; }
        }

        #endregion

        #region Interface

        public override bool IsValid(DateTime now)
        {
            if (!base.IsValid(now))
                return false;

            int second = now.Second;
            int minute = now.Minute;
            int hour = now.Hour;
            int day = now.Day;
            int month = now.Month;
            int wday = (int)now.DayOfWeek;

            if (!_seconds.Test(second))
                return false;

            if (!_minutes.Test(minute))
                return false;

            if (!_hours.Test(hour))
                return false;

            if (!_days.Test(day))
                return false;

            if (!_months.Test(month))
                return false;

            if (!_weekDays.Test(wday))
                return false;

            return true;
        }

        #endregion
    }
}
